﻿// #define DEBUG_AGENT
#if !DEBUG && DEBUG_AGENT
Remove DEBUG_AGENT flag for RELEASE
#endif

using EmperialApps.WeatherSpark.Agent.Internal;
using EmperialApps.WeatherSpark.Data;
using Microsoft.Phone.Scheduler;
using Microsoft.Phone.Shell;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading;
using System.Windows;

namespace EmperialApps.WeatherSpark.Agent {

    /// <summary>Performs periodic scheduled updates to pinned weather tiles.</summary>
    public class ScheduledAgent : ScheduledTaskAgent {

        /// <summary>Initializes a new instance of the <see cref="ScheduledAgent"/> class.</summary>
        public ScheduledAgent( ) {
            if( !ScheduledAgent._classInitialized ) {
                ScheduledAgent._classInitialized = true;
                Logger.DebugPrefix = "AGENT ";

                // Subscribe to the managed exception handler
                Deployment.Current.Dispatcher.BeginInvoke( delegate {
                    Application.Current.UnhandledException += this.ScheduledAgent_UnhandledException;
                } );
            }
        }

        /// <inheritdoc/>
        protected override void OnInvoke( ScheduledTask task ) {
            _deadline = DateTime.UtcNow.AddSeconds( 24 );
            typeof( ScheduledAgent ).Log( string.Format( "Running {0} [{1}], v{2}", task.Description, task.Name, AssemblyInfo.Version ) );
            if( task.LastExitReason != AgentExitReason.None )
                typeof( ScheduledAgent ).Log( "- Last exit reason: " + task.LastExitReason );

            typeof( ScheduledAgent ).Log( "Resetting main tile." );
            var mainTile = ShellTile.ActiveTiles.First( );
            var tileData = new StandardTileData {
                BackgroundImage = SharedStorage.BackgroundImage,
                Title = AssemblyInfo.Name
            };
            mainTile.Update( tileData );

            typeof( ScheduledAgent ).Log( "Loading tile agent settings." );
            ForecastInfo[] forecastInfo;
            if( !SharedStorage.TryLoadForecastInfo( out forecastInfo ) ) {
#if DEBUG
                new InvalidOperationException( "Could not find tile agent settings file." ).Record( );
#endif
            }
            else {
                // Associate each forecast with its tile, if any.
                var activeForecasts = forecastInfo.GroupJoin( ShellTile.ActiveTiles,
                    info => info.Identifier, GetIdentifier, ActiveForecastInfo.Create
                ).ToArray( );

                // Begin forecast downloads.
                typeof( ScheduledAgent ).Log( "Starting forecast downloads." );
                this.BeginDownloads( activeForecasts );

                // Wait for work to complete.
                int timeout = (int)this.Remaining.TotalMilliseconds;
#if DEBUG_AGENT
                if( System.Diagnostics.Debugger.IsAttached )
                    timeout = 60000;
#endif
                if( ActiveForecastInfo.WaitAll( activeForecasts, timeout ) ) {
                    SharedStorage.UpdateForecastInfo( SetLastUpdateValues, activeForecasts );
                }
                else {
                    typeof( ScheduledAgent ).Log( "Cancelling downloads." );
                    lock( this._downloads ) {
                        foreach( WebClient client in this._downloads )
                            client.CancelAsync( );
                        this._downloads.Clear( );
                    }
                }
            }

            typeof( ScheduledAgent ).Log( "Update complete." );
            this.NotifyComplete( );
        }

#if DEBUG
        public override string ToString( ) {
            return "Remaining: " + this.Remaining;
        }
#endif

        #region Private Members

        private static volatile bool _classInitialized;

        private readonly List<WebClient> _downloads = new List<WebClient>( );

        private DateTime _deadline;
        private TimeSpan Remaining { get { return _deadline - DateTime.UtcNow; } }

        private static string GetIdentifier( ShellTile tile ) {
            string uri = tile.NavigationUri.ToString( );
            return ForecastInfo.GetIdentifierFromTileUri( uri );
        }

        private void BeginDownloads( ActiveForecastInfo[] activeForecasts ) {
            // Begin forecast downloads for all active tiles.
            var ready = new List<ActiveForecastInfo>( );
            lock( this._downloads )
                foreach( ActiveForecastInfo active in activeForecasts ) {
                    if( active.BeginDownload( this.OnForecastDownloadOpenReadCompleted ) )
                        this._downloads.Add( active.Client );
                    else
                        ready.Add( active );
                }

            // Perform tile updates immediately where no download occurred.
            foreach( ActiveForecastInfo active in ready ) {
                Forecast forecast;
                if( SharedStorage.TryLoadForecast( active.Info.Location, out forecast ) )
                    UpdateTileImage( active, forecast, updated: false );
            }
        }

        private void OnForecastDownloadOpenReadCompleted( object sender, OpenReadCompletedEventArgs e ) {
            var active = (ActiveForecastInfo)e.UserState;

            // Remove client from list of active downloads.
            bool cancelled;
            lock( this._downloads ) {
                cancelled = e.WasCancelled( ) || this._downloads.Count == 0;
                this._downloads.Remove( active.Client );
            }

            bool forecastUpdated;
            if( cancelled ) {
                typeof( ScheduledAgent ).Log( "Download of " + active + " cancelled." );
                forecastUpdated = false;
            }
            else if( e.Error != null ) {
                typeof( ScheduledAgent ).Log( "Download of " + active + " failed: " + Environment.NewLine + e.Error );
                forecastUpdated = false;
#if DEBUG
                e.Error.Record( active.ToString( ) );
#endif
            }
            else {
                Forecast forecast;
                using( Stream stream = e.Result )
                    try {
                        forecast = Forecast.Load( stream, active.Info.Location );
                        forecastUpdated = true;
                        typeof( ScheduledAgent ).Log( "Download of " + active + " succeeded." );
                    }
                    catch( Exception ex ) {
                        forecast = null;
                        forecastUpdated = false;
                        typeof( ScheduledAgent ).Log( "Load of " + active + " forecast failed: " + Environment.NewLine + ex );
#if DEBUG
                        ex.Record( active.ToString( ) );
#endif
                    }

                if( forecastUpdated ) {
                    // If forecast loaded successfully, save forecast and update tile image.
                    string redirect;
                    if( !forecast.TryGetRedirectUri( out redirect ) ) {
                        Forecast combined = SharedStorage.UpdateForecast( forecast );
                        UpdateTileImage( active, combined, updated: true );
                    }
                    // Otherwise, download from new location.
                    else {
                        lock( this._downloads )
                            if( this._downloads.Count > 0 ) {
                                Uri address = new Uri( redirect );
                                typeof( ScheduledAgent ).Log( "- Redirecting " + active + " to " + address.Host );
                                active.Client.OpenReadAsync( address, active );
                            }
                    }
                }
            }

            if( !forecastUpdated )
                active.SignalCompletion( updated: false );
        }

        private static void UpdateTileImage( ActiveForecastInfo active, Forecast forecast, bool updated ) {
            if( active.Tile != null )
                Deployment.Current.Dispatcher.BeginInvoke( UpdateTileImageCore, active, forecast, updated );
            else
                active.SignalCompletion( updated );
        }

        private static readonly Action<ActiveForecastInfo, Forecast, bool> UpdateTileImageCore = ( active, forecast, updated ) => {
            typeof( ScheduledAgent ).Log( "Updating tile for " + active );

            Units units;
            ForecastDisplayMode mode;
            bool displayBack, displayWide;
            ConvertValue.FromTileMode( active.Info.Mode, out units, out mode, out displayBack, out displayWide );
            uint backgroundColor = active.Info.BackgroundColor;
            var data = new FlipTileData { Title = active.Info.Title };

            var tileImage = SharedStorage.CreateTileImage( null, null, units, mode, forecast, backgroundColor );
            data.BackgroundImage = SharedStorage.SaveTileImage( tileImage );

            tileImage.Title = data.Title;
            tileImage.PathModifier = SharedStorage.SmallTileImagePathModifier;
            data.SmallBackgroundImage = SharedStorage.SaveTileImage( tileImage );

            if( !displayBack ) {
                typeof( ScheduledAgent ).Log( "- Skipping back tile image." );
            }
            else {
                typeof( ScheduledAgent ).Log( "- Creating back tile image." );
                var backTileImage = SharedStorage.CreateTileImage( SharedStorage.FlipTileImagePathModifier, null, units, mode, forecast, backgroundColor );
                data.BackBackgroundImage = SharedStorage.SaveTileImage( backTileImage );
                data.BackTitle = data.Title;

                if( !displayWide ) {
                    typeof( ScheduledAgent ).Log( "- Skipping wide back tile image." );
                }
                else {
                    typeof( ScheduledAgent ).Log( "- Creating wide back tile image." );
                    var wideBackTileImage = SharedStorage.CreateTileImage( SharedStorage.WideFlipTileImagePathModifier, null, units, mode, forecast, backgroundColor );
                    data.WideBackBackgroundImage = SharedStorage.SaveTileImage( wideBackTileImage );
                }
            }

            if( !displayWide ) {
                typeof( ScheduledAgent ).Log( "- Skipping wide tile image." );
            }
            else {
                typeof( ScheduledAgent ).Log( "- Creating wide tile image." );
                var wideTileImage = SharedStorage.CreateTileImage( SharedStorage.WideTileImagePathModifier, null, units, mode, forecast, backgroundColor );
                data.WideBackgroundImage = SharedStorage.SaveTileImage( wideTileImage );
            }

            active.Tile.Update( data );

            active.SignalCompletion( updated );
        };

        private static int IndexOf( ForecastInfo[] forecastInfo, Coordinate location ) {
            for( int i = 0; i < forecastInfo.Length; ++i ) {
                if( forecastInfo[i].Location == location )
                    return i;
            }

            return -1;
        }

        private static ForecastInfo[] SetLastUpdateValues( bool wasRead, ForecastInfo[] existing, ActiveForecastInfo[] activeForecasts ) {
            // If file could not be read, do not try to save.
            if( !wasRead )
                return null;

            // Find all existing tiles that have been updated and refresh their download date.
            foreach( ActiveForecastInfo active in activeForecasts ) {
                DateTimeOffset lastUpdate = active.LastUpdate;
                int index = IndexOf( existing, active.Info.Location );
                if( index >= 0 && existing[index].LastUpdate < lastUpdate )
                    existing[index] = existing[index].Update( lastUpdate: lastUpdate );
            }

            return existing;
        }

        // Code to execute on Unhandled Exceptions
        private void ScheduledAgent_UnhandledException( object sender, ApplicationUnhandledExceptionEventArgs e ) {
            // An unhandled exception has occurred; break into the debugger
            if( System.Diagnostics.Debugger.IsAttached )
                System.Diagnostics.Debugger.Break( );

            e.ExceptionObject.Record( );
        }


        private sealed class ActiveForecastInfo {
            private readonly ManualResetEvent _completedSignal;
            public readonly ForecastInfo Info;
            public readonly ShellTile Tile;

            private ActiveForecastInfo( ForecastInfo info, ShellTile tile ) {
                this.Info = info;
                this.Tile = tile;
                this.LastUpdate = this.Info.LastUpdate;
                this._completedSignal = new ManualResetEvent( initialState: false );
            }

            public static ActiveForecastInfo Create( ForecastInfo info, IEnumerable<ShellTile> tiles ) {
                ShellTile tile = tiles.FirstOrDefault( );
                return new ActiveForecastInfo( info, tile );
            }

            public WebClient Client { get; private set; }
            public DateTimeOffset LastUpdate { get; private set; }


            public static bool WaitAll( ActiveForecastInfo[] activeForecasts, int millisecondsTimeout ) {
                // http://stackoverflow.com/questions/8412549/waitall-leads-to-notsupportedexception-for-windows-phone-7-1
                int start = Environment.TickCount;
                foreach( ActiveForecastInfo active in activeForecasts ) {
                    int elapsed = Environment.TickCount - start;
                    int wait = millisecondsTimeout - elapsed;
                    if( wait < 0 || !active._completedSignal.WaitOne( wait ) )
                        return false;
                }

                return true;
            }

            public bool BeginDownload( OpenReadCompletedEventHandler openHandler ) {
                string downloadUri = this.Info.DownloadUri;
                DateTimeOffset updateThreshold = this.LastUpdate + this.Info.RefreshRate;
                bool download = downloadUri != null && updateThreshold <= DateTimeOffset.Now;
                string status = download ? "- Beginning" : "- Skipping";
                typeof( ScheduledAgent ).Log( status + " download for " + this );

                if( download ) {
                    Uri address = new Uri( downloadUri );
                    this.Client = new WebClient( );
                    this.Client.OpenReadCompleted += openHandler;
                    this.Client.OpenReadAsync( address, this );
                }

                return download;
            }

            public void SignalCompletion( bool updated ) {
                if( updated )
                    this.LastUpdate = DateTimeOffset.Now;

                this._completedSignal.Set( );
            }

            public override string ToString( ) {
#if DEBUG
                string updated = this._completedSignal.WaitOne( 0 ) ? "+" : "-";
                return updated + "{" + this.Info + "}";
#else
                return this.Info.Identifier ?? "-";
#endif
            }
        }

        #endregion

    }

}
